/*
 * Copyright (c) 1994, 2021, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package com.i360day.invoker.http.httpclient;

import com.i360day.invoker.BodyTemplate;
import com.i360day.invoker.http.InvokerClient;
import com.i360day.invoker.http.Request;
import com.i360day.invoker.http.Response;
import com.i360day.invoker.properties.HttpInvokerProperties;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.NoHttpResponseException;
import org.apache.http.StatusLine;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.RequestBuilder;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Map;

/**
 * @author: liju.z
 * @date: 2024/2/5 3:47
 */
public class InvokerHttpClient implements InvokerClient {

    private Logger logger = LoggerFactory.getLogger(InvokerHttpClient.class);

    protected static final String HTTP_HEADER_CONTENT_ENCODING = "Content-Encoding";

    protected static final String ENCODING_GZIP = "gzip";

    private CloseableHttpClient httpClient;
    /**
     * 默认最大总连接数
     */
    private static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 100;
    /**
     * 每条路由的默认最大连接数
     */
    private static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 10;
    private final PlainConnectionSocketFactory plainConnectionSocketFactory;
    private final SSLConnectionSocketFactory sslConnectionSocketFactory;
    private final RequestConfig requestConfig;

    public static InvokerHttpClient create(HttpInvokerProperties properties) {
        return new InvokerHttpClient(properties);
    }


    public InvokerHttpClient(HttpInvokerProperties properties) {
        this(PlainConnectionSocketFactory.getSocketFactory(), SSLConnectionSocketFactory.getSocketFactory(), properties);
    }

    public InvokerHttpClient(PlainConnectionSocketFactory plainConnectionSocketFactory, SSLConnectionSocketFactory sslConnectionSocketFactory, HttpInvokerProperties properties) {
        this.plainConnectionSocketFactory = plainConnectionSocketFactory == null ? PlainConnectionSocketFactory.getSocketFactory() : plainConnectionSocketFactory;
        this.sslConnectionSocketFactory = sslConnectionSocketFactory == null ? SSLConnectionSocketFactory.getSocketFactory() : sslConnectionSocketFactory;
        this.requestConfig = RequestConfig.custom()
                .setConnectTimeout(properties.getConnectTimeout())
                .setConnectionRequestTimeout(properties.getConnectionRequestTimeout())
                .setSocketTimeout(properties.getReadTimeout())
                .build();;

        HttpInvokerProperties.HttpInvokerRequestProperties request = properties.getRequest();

        Registry<ConnectionSocketFactory> schemeRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", this.plainConnectionSocketFactory)
                .register("https", this.sslConnectionSocketFactory)
                .build();

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(schemeRegistry);
        connectionManager.setMaxTotal(request.getMaxConnections());
        connectionManager.setDefaultMaxPerRoute(DEFAULT_MAX_CONNECTIONS_PER_ROUTE);
        httpClient = HttpClientBuilder.create().setConnectionManager(connectionManager).build();
    }

    @Override
    public void close() {
        try {
            httpClient.close();
        } catch (IOException e) {
            //ignore
        }
    }

    /**
     * 执行
     *
     * @param request
     * @return
     * @throws IOException
     */
    @Override
    public Response execute(Request request) throws IOException {
        //request
        HttpResponse httpResponse = convertAndSend(request);
        validateResponse(httpResponse);
        //body
        InputStream inputStream = httpResponse.getEntity().getContent();
        Response.Body body;
        if (isGzipResponse(httpResponse)) {
            body = new Response.GZIPInputStream(inputStream, inputStream.available());
        } else {
            body = new Response.InputStreamBody(inputStream, inputStream.available());
        }
        //builder
        Response.Builder builder = Response.Builder.create();
        //header
        Header[] allHeaders = httpResponse.getAllHeaders();
        for (Header header : allHeaders) {
            builder.header(header.getName(), header.getValue());
        }
        //response
        return builder
                .status(httpResponse.getStatusLine().getStatusCode())
                .reason(httpResponse.getStatusLine().getReasonPhrase())
                .body(body)
                .request(request)
                .build();
    }

    /**
     * 识别response
     *
     * @param httpResponse
     * @return
     */
    boolean isGzipResponse(HttpResponse httpResponse) {
        Header encodingHeader = httpResponse.getFirstHeader(HTTP_HEADER_CONTENT_ENCODING);
        return (encodingHeader != null && encodingHeader.getValue() != null &&
                encodingHeader.getValue().toLowerCase().contains(ENCODING_GZIP));
    }

    /**
     * 验证response
     *
     * @param response
     * @throws IOException
     */
    void validateResponse(HttpResponse response) throws IOException {
        StatusLine status = response.getStatusLine();
        if (status.getStatusCode() >= 300) {
            String serverErrorContent = IOUtils.toString(response.getEntity().getContent(), Charset.defaultCharset());
            logger.error("http invoker server error {}", serverErrorContent);
            throw new NoHttpResponseException(
                    String.format(
                            "Did not receive successful HTTP response: status code = %s status message = [%s] ",
                            status.getStatusCode(),
                            status.getReasonPhrase()
                    )
            );
        }
    }

    /**
     * 发送请求
     *
     * @param request
     * @return
     * @throws IOException
     */
    HttpResponse convertAndSend(Request request) throws IOException {

        HttpUriRequest httpRequest = convertHttpRequest(request, requestConfig);

        return httpClient.execute(httpRequest);
    }

    /**
     * @param request
     * @return
     */
    HttpUriRequest convertHttpRequest(Request request, RequestConfig config) {
        //request
        RequestBuilder requestBuilder = RequestBuilder.post(request.getUri());
        //header
        Map<String, Collection<String>> headers = request.headers();
        headers.forEach((name, values) -> {
            if (values != null && values.size() > 0) {
                values.forEach(value -> {
                    requestBuilder.addHeader(name, value);
                });
            } else {
                requestBuilder.addHeader(name, "");
            }
        });
        //data
        BodyTemplate body = request.body();
        ByteArrayEntity byteArrayEntity = new ByteArrayEntity(body.getBody());
        //body
        return requestBuilder
                .setEntity(byteArrayEntity)
                .setConfig(config)
                .build();
    }
}
